node.js 工作原理

Runtime system

运行时系统(Runtime system),又称之为 “运行环境”,指将半编译的代码运行的环境。在 node 核心概念中指的是 数据类型的确定 由编译时推迟到了运行时,因此就出现了运行时系统(Rutime System)来处理编译后的代码。

因此这个概念和流程就是 JavaScript 引擎负责解析和即时编译(JIT),结合 Rutime 因此可以实现了 Window 、DOM API,存在与浏览器的 Runtime 中。

所以 Node.js 中的 Runtime 也包含了不同的库,例如 Cluster、FileSystem API,这里面的 Runtime 都包含了内置的数据类型和常用的工具,因此 Chrome 和 Node.js 同样使用 V8 引擎但不会使用同一个 Runtime。

Cluster (集群)在 node v0.8 开始集成的内置模块,为解决单线程设计的瓶颈而生。

System Runtime :https://designfirst.io/systemruntime/,常见的运行时系统需要通过 C 或汇编而成,通过此项目可以简单构建

VM

VM 是 Node 核心模块之一,通过使用 V8 的 Virtual Machine Contexts 动态编译和执行代码,但代码的运行在上下文中是与当前隔离的,但他不是安全的,因此不要使用他来运行不受信任的代码。

不同与浏览器的沙箱环境, vm 提供了一系列的 API 用于在 V8 虚拟机中编译和运行代码,可以让 JavaScript 的代码被编译且立即运行或编译后保存运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义常量
const util = require('util');
const vm = require('vm');

// 创建一个新的 vm.Script 对象,将会编译 code 但不会运行,之后可以多次运行。
const script = new vm.Script('globalVar += 1; anotherGlobalVar = 1;');

// 创建沙盒并绑定上下文
const sandbox = {globalVar: 1};

// 创建上下文并将沙盒(sandbox)绑定至到环境中
const contextifiedSandbox = vm.createContext(sandbox);

// 运行 vm.script 内包含的上下文对象,并返回结果
const result = script.runInContext(contextifiedSandbox);

// 是否沙盒绝对等于上下文沙盒 (true)
console.log(`sandbox === contextifiedSandbox ? ${sandbox === contextifiedSandbox}`);

// sandbox: { globalVar: 2, anotherGlobalVar: 1 }
console.log(`sandbox: ${util.inspect(sandbox)}`);

// result: 1
console.log(`result: ${util.inspect(result)}`);

vm.Script 是新增于 node.js v0.3.1 类,用于绑定任何全局对象,之后可以多次运行,也就是说我们将 sandbox 对象创建了环境的上下文,将对象绑定到环境中,因此最后输出 globalVar: 2, anotherGlobalVar: 1

这也就表明了 script.runInContextcontextifiedSandbox 上下文中运行,从而当 console.log 时,他的值已经改变了。

util.inspect 用于检查常量,除了 script.runInContext 以外,在 node.js v6.3.0 中也完全支持了更为简单的方法 vm.runlnContext

1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = require('vm')

// 设置上下文全局变量为 1
const context = {globalVar: 1};

// 创建上下文方法准备该对象,以便可以调用上下文
vm.createContext(context);

// 通过 vm 方法编译 code,并运行上下文并返回结果
vm.runInContext('globalVar += 1;', context)

// globalVar:2
console.log('globalVar:' + context.globalVar);

回调\同步\异步\阻塞\非阻塞

回调函数

在计算机的设计之初,基本上都是通过异步来进行的,在此之前我们需要知道异步的相关类别

Name Info
同步 按照需求内的一步一步进行执行
异步 可以同时执行多种事情
堵塞 按照需求内一步一步进行,当一个程序没有执行,下面所排队等待的程序都处于排队状态
非堵塞 当一个程序没有执行完毕,后面所排队的程序都会进行插队

node.js 异步内的直接体现就在于回调,而回调就是,当你不知道用户何时会点击按钮,因此你为事件所定义了一个事件的处理程序,该事件会在处理时被触发调用即“回调”,我们以文件读取为例:

1
2
3
4
5
6
7
8
9
10
var fs = require('fs');

// 异步读取文件内容,通过 readFile() 缓冲整个文件
fs.readFile('package.json', function (err,data) {
if (err) {
console.log(err.stack);
return;
}
console.log(data.toString() + '\n File echo success!');
});

以上的 js 脚本中那个,主要通过 fs.readFile 函数来异步读取文件,如果文件不存在则直接通过 err 输出错误信息,如没有发生错误则会忽略 err对象进行输出,因此文件的内容就通过了回调函数输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ node vm.js 
{
"name": "demo",
"version": "1.0.0",
"description": "node demo",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gitee.com/sif_one/node-demo"
},
"keywords": [
"null"
],
"author": "sun likun",
"license": "ISC"
}

File echo success!

假设 package.json 文件并不存在,那么在经过 s.readFile() 方法时,err 对象就会输出错误信息:

1
2
$ node vm.js 
Error: ENOENT: no such file or directory, open 'pac1kage.json'

同步和异步的区别

同步可以说是一个进程在执行某一个请求的时候,另一个进程则需要等待,等上一个请求完后,这个进程才可以执行。而异步则是另一个进程无需等待,无论其他进程状态是否,直接进程执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function __test() {
setTimeout(()=> {
console.log(1000);
},1000)
}

async function test() {
for (let i=0; i<15; i++) {
try {
await __test();
} catch (e) {
console.log('a', e);
}
}
}
test();

首先我们需要注意的是,为了防止异步的出错,需要将 await 放到 try……catch 中,这就让 __testtest 两个函数同步起来,这样就会让输出的结果是__test 运行完了,test 还在等待的效果,正因这样,一个正常的运行结果才不会混乱。

堵塞

堵塞与非堵塞

1
2
3
4
5
6
7
8
9
10
11
12
var fs = require("fs")

var data = fs.readFileSync('hello.text')
fs.readFile('hello.text', (err,data)=> {
if (err != null) {
console.log('没有此文件')
return
}
})

console.log(data.toString())
console.log("程序执行结束!")

输出:

1
2
hello
程序执行结束!

在以上的code中,首先并不存在 hesllo.text 文件,处理回调错误的同时,在node中,任何回调函数中的地一个参数都为错误对象在Node官方文档中被标注为错误优先与回调的说法。

非堵塞

1
2
3
4
5
6
7
8
9
10
var fs = require("fs")

fs.readFile("hello.text", function(err,data) {
if (err !== null) {
console.log(err)
return
}
console.log(data.toString())
})
console.log('程序执行完成')

输出:

1
2
程序执行完成
hello

本文通过堵塞与非堵塞来进行演示,通常情况下堵塞代码是按照需求一步一步的执行,当一个步骤或没有完成时等待的程序处于排队状态,而非堵塞则是不当一步骤没有执行完毕后面所排队的程序都会依次进行插队即处理更加的效率

事件循环

Node.js 基本上所以的事件机制都使用类似与设计模式中的观察者模式实现,而观察者模式又是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个观察者的对象或其他对象。

观察者模式会在他本身的状态改变时发出通知,而这种呼叫各类观察者所提供的方式来实现,因此这种模式也被称之为事件处理系统。

在 node.js 中,所提供了 events 库,而他最主要的部分就是创建对象、事件处理、绑定事件以及最后的触发事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var events = require('events');

// 创建事件
var eventEmitter = new events.EventEmitter();

// 创建事件处理
var connectHandler = function connected() {
console.log('1.连接成功')

// 触发 data_received 事件
eventEmitter.emit('data_received');
}

// 将 connectHandler 绑定 connection
eventEmitter.on('connection', connectHandler);

// 绑定 'data_receiver' 事件
eventEmitter.on('data_received', function () {
console.log("2.接受触发成功\n3.流程执行完毕")
});

eventEmitter.emit('connection')
1
2
3
1.连接成功
2.接受触发成功
3.流程执行完毕

上述 code 中 eventEmitter.emit() 用于触发事件,同时他也允许将任意一组参数传递给监听器函数,之后通过eventEmitter.on() 函数来注册监听器 connection ,最后通过 data_received 的事件绑定被监听器发现,从而输出整套流程。

本文使用《江雪分析公开知识存储库知识共享许可证》进行发布